package squareBorderPackage;

import java.util.HashMap;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of borders involving two squares in two opposed directions.
 * 
 * @invar 	The squares of each border must be proper squares.
 * 			|hasProperSquares()
 * 
 * @author	Esther Alberts and Jessa Bekker
 * @version	2.0
 */
public abstract class Border {

	/**
	 * Initialize this border with the given squares and directionFirstSquare.
	 * 
	 * @param 	squares
	 * 			The new squares to which this border must be attached.
	 * @param 	directionOfFirstSquare
	 * 			The new directionOfFirstSquare of this border
	 * @effect 	...
	 * 			| attachTo(squares, directionOfFirstSquare);
	 * @effect	...
	 * 			| setDirections(directionOfFirstSquare);
	 */
	public Border(Square square, Direction directionOfFirstSquare){
		setSquareDirections(directionOfFirstSquare);
		attachTo(square, directionOfFirstSquare);
	}

	/**
	* Terminate this border.
	*
	* @post 	This border is terminated.
	* 			| new.isTerminated()
	* @post		This border no longer references a square in any direction
	* 			as the square to which it is attached.
	* 			| for each square in squares :
	* 			|	square == null
	* @post		If this border was not already terminated, the squares to which
	* 			this border was attached in a direction, no longer has a border
	* 			attached to it in that direction.
	* 			| if(! isTerminated())
	* 			|	then (! (new getSquareAt(direction)).hasBorderAt(direction)
	*/
	
	public void terminate() {
		if (!isTerminated()) {
			Square[] formerSquares = getSquares();
			isTerminated = true;
			for(int i = 0; i<=1; i++) {
			setSquare(i, null);
				if(formerSquares[i] != null){
					formerSquares[i].setBorderAt(null, getSquareDirections()[i]);
					formerSquares[i].updateSpace();
				}
			}
		}
	}
	

	/**
	 * Check whether this border is terminated.
	 */
	@Basic @Raw
	public boolean isTerminated() {
		return this.isTerminated;
	}

	/**
	 * Variable registering whether this border is terminated.
	 */
	private boolean isTerminated;

	/**
	 * Return the square which has in the given direction this border.
	 * 
	 * @param	direction
	 * 			The direction in which the square references this border.
	 * @throws	IllegalArgumentException
	 * 			This border has no square which has this border in the given direction
	 * 			| !(getSquares()[0].getBorders().get(direction)==this 
	 * 			|	|| getSquares()[1].getBorders().get(direction)==this)
	 */
	public Square getSquareAt(Direction direction) throws IllegalArgumentException {
		if(!isASquareDirection(direction))
				throw new IllegalArgumentException(
					"This border has no square which has this border in the given direction!");
		return getSquares()[getIndexOf(direction)];
	}

	/**
	 * Return the squares to which this border is attached.
	 */
	@Basic @Raw
	public Square[] getSquares() {
		return squares.clone();
	}

	/**
	 * Check whether the given square is attached to this border.
	 * 
	 * @param 	square
	 *          The square to be checked.
	 * @return 	Return true if and only if the given square is attached to this border. 
	 * 			| result == (getSquares()[0] == square || getSquares()[1] == square)
	 */
	public boolean hasAsSquare(Square square) {
		return (getSquares()[0] == square || getSquares()[1] == square);
	}

	/**
	 * Check whether this border can have the given squares as its squares.
	 * 
	 * @param	squares
	 * 			The squares to check.
	 * @return	False if the array does not contains exactly 2 squares.
	 * 			| if(squares.length != 2)
	 * 			|	then ( result == false)
	 * @return	If this border is terminated, true if and only if both squares are not effective.
	 * 			| if(isTerminated)
	 * 			|	then (result == (squares[0]==null && squares[1]==null))
	 * @return	False if one of the squares is effective and terminated.
	 * 			| if ( (squares[0]!=null && squares[0].isTerminated())
	 *			|		|| (squares[1]!=null && squares[1].isTerminated()))
	 *			|	then (result == false)
	 * @return	False if the given array reference the same squares (or null).
	 * 			| if (squares[0] != squares[1])
	 * 			|	then (result == false)
	 * @return	False if this is a door and one of the square directions is floor.
	 * 			| if (this instanceof Door
	 *			|	&& (getSquareDirections()[0] == Direction.FLOOR
	 *			|		|| getSquareDirections()[1] == Direction.FLOOR))
	 *			|	then (result == false)
	 * @return	False if one of the squares would have more than the maximum number of doors
	 * 			if it was attached to this border.
	 * 			| if (this instanceof Door && (getSquareDirections()[0] == Direction.FLOOR
	 *			|		|| getSquareDirections()[1] == Direction.FLOOR))
	 *			| 	then (result == false)
	 * @return	False if on of the squares does not reference at least one wall or door as border
	 * 			| if for some square of squares:	
	 * 			|		for some direction of borders.keySet():
	 * 			|			square.getBorders().get(direction) instance of wall
	 * 			|			|| |			square.getBorders().get(direction) instance of
	 * 			|	result == false
	 */
	
	public boolean canHaveAsSquares(@Raw Square[] squares){
		if(squares.length != 2)
			return false;
		
		if(isTerminated())
			return (squares[0]==null && squares[1]==null);
		
		if ( (squares[0]!=null && squares[0].isTerminated())
				|| (squares[1]!=null && squares[1].isTerminated()))
				return false;
		
		if (squares[0] == squares[1])
			return false;
		
		if(this instanceof Door
				&& (getSquareDirections()[0] == Direction.FLOOR
						|| getSquareDirections()[1] == Direction.FLOOR))
			return false;
		
		for(int i = 0 ; i<= 1; i++){
			if(squares[i] != null){
			HashMap<Direction, Border> borders = squares[i].getBorders();
				borders.put(getSquareDirections()[i], this);
				assert(borders.containsValue(this));
				if(! squares[i].areAppropriateBorders(borders))
					return false;
			}
		}
			
		return true;
	}
	
	/**
	 * Check whether the given square can be attached to this Border
	 * 
	 * @param 	square
	 * 			The square to check for.
	 * @param	direction
	 * 			The direction in which the given square should reference this border.
	 * @return	False if the given square is terminated.
	 * 			| if (square.isTerminated())
	 * 			|	then (result == false)
	 * @return	True if this border can have the array of borders formed by attaching
	 * 			the given square to this border as its borders.
	 * 			| take squares = getSquares()
	 * 			| take squares[getIndexOfAttachement()] = square
	 * 			| result == canHaveAsSquares(squares)
	 */
	public boolean canAttachSquare(Square square, Direction direction) {
		if(!isASquareDirection(direction))
			return false;
		Square[] squares = getSquares();
		squares[getIndexOf(direction)] = square;
		return canHaveAsSquares(squares);
		
	}

	/**
	 * Check whether this border has proper squares attached to it.
	 * 
	 * @return	True if and only if 
	 * 				this border can have this squares as his squares
	 * 			and if
	 * 				any square referenced by this border in turn references this border as the border
	 * 				to which it is attached to or is ineffective.
	 * 			|result == 
	 * 			|	canHaveAsSquares(getSquares()) &&
	 * 			|	(getSquares()[0]==null || getSquares()[0].getBorders().containsValue(this)) &&
	 *			|	(getSquares()[1]==null || getSquares()[1].getBorders().containsValue(this))
	 */
	public boolean hasProperSquares(){
		return 	canHaveAsSquares(getSquares()) &&
				(getSquares()[0]==null || getSquares()[0].getBorderAt(getSquareDirections()[0]).equals(this)) &&
				(getSquares()[1]==null || getSquares()[1].getBorderAt(getSquareDirections()[1]).equals(this));
	}

	/**
	 *  Remove a square of this border.
	 * 
	 * @param	direction
	 * 			The direction of the square to remove
	 * @throws	IllegalArgumentException
	 * 			The given direction is not a square direction.
	 * 			| ! isASquareDirection(direction)
	 * @post	This border does not references the given square.
	 * 			| ! new.hasAsSquare(square)
	 * @post	The given square does not references this border.
	 * 			| ! (new square).hasAsBorder(this)
	 * @effect	If this border references to the given square and to a non effective square,
	 * 			this border will be terminated.
	 * 			| if(hasAsSquare(square) && hasAsSquare(null))
	 * 			|	then terminate()
	 */
	public void removeSquareAt(Direction direction)
	throws IllegalArgumentException {
		if(!isASquareDirection(direction))
			throw new IllegalArgumentException
			("No square references this border in the given direction!");
		if(getSquareAt(direction) != null) {
			if(getSquareAt(direction.getOpposite()) == null)
				this.terminate();
			else {
				int i;
				if(getSquareDirections()[0].equals(direction))
					i = 0;
				else
					i = 1;
				Square formerSquare = getSquares()[i];
				setSquare(i, null);
				formerSquare.setBorderAt(null, direction);
				formerSquare.updateSpace();
				getSquareAt(direction.getOpposite()).updateSpace();
			}
		}
	}

	/**
	 * Set the given square at the given index.
	 * 
	 * @param 	index
	 * 			The index to set the square to.
	 * @param 	square
	 * 			The square to set.
	 */
	@Raw
	private void setSquare(int index, Square square){
		squares[index] = square;
	}

	/**
	 * Attach the given square to this border.
	 * 
	 * @param	square
	 * 			The square to which this border must be attached.
	 * @param	direction
	 * 			The direction in which the given square must reference this border.
	 * @post	This border references to the given square in the given direction.
	 * 			| new.getSquareAt(direction).equals(square)
	 * @post	The given square references to this border in the given direction.
	 * 			| (new square).getBorderAt(direction).equals(this)
	 * @effect	If there are two squares attached to the new border and this border is no obstacle,
	 * 			the two squares will be merged.
	 * 			| if (new.getSquares()[0] != null && new.getSquares()[1] != null
	 * 			|		&& !this.isObstacle())
	 * 			|	then getSquares()[0].mergeWith(getSquares()[1]
	 * @throws	IllegalStateException
	 * 			This border is already terminated
	 * 			|this.isTerminated()
	 * @throws	IllegalArgumentException
	 * 			The given square can not be attached to this border in the given direction.
	 * 			|!canAttachSquare(squares)
	 */
	public void attachTo(Square square, Direction direction) 
	throws IllegalArgumentException, IllegalStateException{
		if (this.isTerminated())
			throw new IllegalStateException("Terminated border!");
		if (! this.canAttachSquare(square, direction))
			throw new IllegalArgumentException
			("The given square can not be attached to this border in the given direction!");
		
		if(getSquareAt(direction) != square){
			Square formerSquare = getSquareAt(direction);
			setSquare(getIndexOf(direction), square);
			if(square != null) {
				if(square.getBorderAt(direction)!=null)
					square.getBorderAt(direction).removeSquareAt(direction);
				square.setBorderAt(this, direction);
			}
			if(formerSquare != null)
				formerSquare.setBorderAt(null, direction);
			if(!this.isObstacle() && getSquares()[0] != null && getSquares()[1] != null)
				((AccessibleSquare) getSquares()[0]).mergeWith(((AccessibleSquare) getSquares()[1]));
		}
	}
	
	/**
	 * Variable referencing the squares to which this border is tangent.
	 */
	private Square squares[] = new Square[2];

	/**
	 * Return the direction in which squares[0] references this border.
	 */
	public Direction[] getSquareDirections() {
		return squareDirections.clone();
	}
	
	public int getIndexOf(Direction direction) 
	throws IllegalArgumentException{
		if(!isASquareDirection(direction))
			throw new IllegalArgumentException("The given direction is not applicable");
		if(getSquareDirections()[0].equals(direction))
			return 0;
		return 1;
	}
	
	/**
	 * Check whether or not a square references this border in the given direction.
	 * 
	 * @param 	direction
	 * 			The direction to check.
	 * @return	True if and only if the given direction is one of the square directions.
	 * 			| result == (getSquareDirections()[0] == direction ||
	 *			| getSquareDirections()[1] == direction)
	 */
	public boolean isASquareDirection(Direction direction) {
		return(getSquareDirections()[0] == direction ||
				getSquareDirections()[1] == direction);
	}
	
	/**
	 * Set the direction in which the squares reference this border.
	 * 
	 * @param 	directionOfFirstSquare
	 * 			The direction in which the first square references this border.
	 * @post	The first square references this border in the given direction
	 * 			and the second square references this border in the opposite direction.
	 * 			|	getSquareDirections()[0] == directionOfFirstSquare &&
	 * 			|	getSquareDirections()[1] == directionOfFirstSquare.getOpposite()
	 */
	private void setSquareDirections(Direction directionOfFirstSquare) {
		squareDirections[0] = directionOfFirstSquare;
		squareDirections[1] = directionOfFirstSquare.getOpposite();
	}
	
	/**
	 * Variable referencing the direction in which squares[0] references this border.
	 */
	private final Direction[] squareDirections = new Direction[2];

	
	public abstract boolean getSlippery();

	public abstract boolean isObstacle();

	/**
	 * Return the strongest border of this border and the given border.
	 * 
	 * @param	other
	 * 			The border to compare this border with
	 */
	public abstract Border getStrongestBorder(Border other);
	
	
	/**
	 * Split this border in two borders
	 * 
	 *  @pre	This border must have two squares.
	 *  		| !(getSquares()[0] == null || getSquares()[1] == null)
	 *  @return	An array with the two new Borders
	 */
	public abstract Border[] split();
	
	

}
